﻿using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
using System.Net;
using linkedin_bot_demo.Models;
using System.IO;


namespace linkedin_bot_demo.Service
{
    public class LinkedinApiClient : Common.Disposable
    {
        private const string API_KEY = "77slv7rweme81w";
        private const string API_SECRET = "kg4UUHYzDlnSKGtQ";

        private const int CALLBACK_PORT0 = 50000;
        private const int CALLBACK_PORT1 = 50001;
        private const int CALLBACK_PORT2 = 50002;
        private const int CALLBACK_PORT3 = 50003;
        private const int CALLBACK_PORT4 = 50004;
        private const int CALLBACK_PORT5 = 50005;

        private readonly string _accessToken;
        private readonly int _expires;

        public LinkedinApiClient(string accessToken)
        {
            if (accessToken == null)
            {
                throw new ArgumentNullException();
            }

            _accessToken = accessToken;
        }

        public LinkedinApiClient() : this(API_KEY, API_SECRET) { }

        public LinkedinApiClient(string AppApiKey, string AppApiSecret)
        {
            string authorizationCode = GetAuthorizationCode(AppApiKey, new string[] { "r_network", "rw_groups" });
            System.Windows.Clipboard.SetText(authorizationCode);

            Tuple<string, int> accessTokenAndExpires = GetAccessToken(AppApiKey, AppApiSecret, authorizationCode);
            _accessToken = accessTokenAndExpires.Item1;
            _expires = accessTokenAndExpires.Item2;
        }

        public string AccessToken { get { return _accessToken; } }
        public int Expires { get { return _expires; } }

        public IEnumerable<GroupModel> GetGroups()
        {
            using (ApiGetInvoker invoker = CreateApiGetInvoker("/v1/people/~/group-memberships"))
            {
                XDocument xml = invoker.Invoke();

                IEnumerable<XElement> groupElements = xml.XPathSelectElements("/group-memberships/group-membership/group");
                List<GroupModel> groups = new List<GroupModel>();
                foreach (XElement groupElement in groupElements)
                {
                    GroupModel group = new GroupModel()
                    {
                        Id = int.Parse(groupElement.XPathSelectElement("id").Value),
                        Name = groupElement.XPathSelectElement("name").Value
                    };

                    groups.Add(group);
                }

                return groups;
            }
        }

        public MyProfileModel GetMyProfile()
        {
            using (ApiGetInvoker invoker = CreateApiGetInvoker("/v1/people/~"))
            {
                XDocument xml = invoker.Invoke();

                return new MyProfileModel()
                {
                    FirstName = xml.XPathSelectElement("/person/first-name").Value,
                    LastName = xml.XPathSelectElement("/person/last-name").Value
                };
            }
        }

        public void CreateGroupPost(int groupId, string title, string summary)
        {
            using (ApiPostInvoker invoker = CreateApiPostInvoker("/v1/groups/" + groupId + "/posts"))
            {
                XDocument body = XDocument.Parse("<?xml version='1.0' encoding='UTF-8'?><post><title></title><summary></summary></post>");
                body.XPathSelectElement("/post/title").SetValue(title);
                body.XPathSelectElement("/post/summary").SetValue(summary);
                invoker.Invoke(body);
            }
        }

        //public void PostNetworkUpdates(string body)
        //{
        //    using (ApiPostInvoker invoker = CreateApiPostInvoker("/v1/people/~/person-activities"))
        //    {
        //        XDocument bodyXml = XDocument.Parse("<?xml version='1.0' encoding='UTF-8'?><activity locale=\"en_US\"><content-type>linkedin-html</content-type><body></body></activity>");
        //        bodyXml.XPathSelectElement("/activity/body").SetValue(body);
        //        invoker.Invoke(bodyXml);
        //    }
        //}

        private static string GetAuthorizationCode(string AppApiKey, IEnumerable<string> scopes)
        {
            Dictionary<string, string> authorizationCodeParams = null;
            using (LocalHostHttpServer server = new LocalHostHttpServer(new int[] {
                CALLBACK_PORT0,
                CALLBACK_PORT1,
                CALLBACK_PORT2,
                CALLBACK_PORT3,
                CALLBACK_PORT4,
                CALLBACK_PORT5 },
                (request, response, utility) =>
                {
                    authorizationCodeParams =
                                    request.Params.AllKeys.ToDictionary(key => key, value => request.Params[value]);

                    if (response != null)
                    {
                        string fullTypeName = typeof(LinkedinApiClient).FullName;
                        using(Stream htmlPageStream = typeof(LinkedinApiClient).Assembly.GetManifestResourceStream(fullTypeName + ".GetAuthorizationCodeComplete.html"))
                        using (var writer = new StreamWriter(response.OutputStream))
                        {
                            htmlPageStream.CopyTo(response.OutputStream);
                        }

                        SetForegroundWindow(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle);
                    }
                }))
            {
                string friendlyScopes = string.Join(" ", scopes.ToArray());

                string urlStr = "https://www.linkedin.com/uas/oauth2/authorization?response_type=code"
                                                + "&client_id=" + AppApiKey
                                                + "&scope=" + Uri.EscapeDataString(friendlyScopes)
                                                + "&state=" + Guid.NewGuid().ToString()
                                                + "&redirect_uri=" + Uri.EscapeDataString(server.ListenUri.AbsoluteUri);

                Uri authorizationCodeURL = new Uri(urlStr);

                System.Diagnostics.Process.Start(authorizationCodeURL.AbsoluteUri);

                server.WaitCallBack(TimeSpan.FromMinutes(3));
                if (authorizationCodeParams == null)
                {
                    throw new Exception("No response");
                }

                if (authorizationCodeParams.ContainsKey("error"))
                {
                    string msg = authorizationCodeParams["error"];
                    if (authorizationCodeParams.ContainsKey("error_description"))
                    {
                        msg = msg + ": " + authorizationCodeParams["error_description"];
                    }

                    throw new Exception("Cannot get authorization code.\r\n" + msg);
                }

                if (!authorizationCodeParams.ContainsKey("code"))
                {
                    throw new Exception("Bad response. Auth service did not return 'code'");
                }

                string authorizationCode = authorizationCodeParams["code"];

                return authorizationCode;
            }
        }

        private static Tuple<string, int> GetAccessToken(string AppApiKey, string AppApiSecret, string AuthorizationCode)
        {
            Dictionary<string, string> authorizationCodeParams = null;
            using (LocalHostHttpServer server = new LocalHostHttpServer(new int[] {
                CALLBACK_PORT0,
                CALLBACK_PORT1,
                CALLBACK_PORT2,
                CALLBACK_PORT3,
                CALLBACK_PORT4,
                CALLBACK_PORT5 },
                (request, response, utility) =>
                {
                    authorizationCodeParams = 
                        request.Params.AllKeys.ToDictionary(key => key, value => request.Params[value]);
                }))
            {
                var url = "https://www.linkedin.com/uas/oauth2/accessToken"
 + "?grant_type=authorization_code"
 + "&code=" + AuthorizationCode
 + "&redirect_uri=" + Uri.EscapeDataString(server.ListenUri.AbsoluteUri)
 + "&client_id=" + AppApiKey
 + "&client_secret=" + AppApiSecret;

                using (var httpClient = new WebClient())
                {
                    var response = httpClient.UploadString(url, "");
                    var jsonString = response;

                    var json = JObject.Parse(jsonString);

                    var accessToken = json["access_token"];
                    var expires = json["expires_in"];

                    if (accessToken != null)
                    {
                        return new Tuple<string, int>(accessToken.ToString(), int.Parse(expires.ToString()));
                    }

                    string msg = "Unknown";

                    var errorValue = json["error"];
                    if (errorValue != null)
                    {
                        var errorDesrValue = json["error_description"];

                        msg = errorValue.ToString();
                        if (errorDesrValue != null)
                        {
                            msg = msg + ": " + errorDesrValue.ToString();
                        }
                    }

                    throw new Exception("Cannot get access token.\r\n" + msg);
                }
            }
        }

        private ApiGetInvoker CreateApiGetInvoker(string apiMethod)
        {
            return new ApiGetInvoker(this, apiMethod);
        }

        private ApiPostInvoker CreateApiPostInvoker(string apiMethod)
        {
            return new ApiPostInvoker(this, apiMethod);
        }

        private class LocalHostHttpServer : IDisposable
        {
            private readonly NHttp.HttpServer _server;
            private readonly int _port;
            private readonly Uri _listenUrl;
            private readonly ManualResetEvent _eventRedirectComplete = new ManualResetEvent(false);
            private readonly Action<NHttp.HttpRequest, NHttp.HttpResponse, NHttp.HttpServerUtility> _userHandler;

            public LocalHostHttpServer(IEnumerable<int> tryPorts, Action<NHttp.HttpRequest, NHttp.HttpResponse, NHttp.HttpServerUtility> userHandler)
            {
                var connection = CreateAndStartHTTPServer(tryPorts, RequestHandler);
                _server = connection.Item1;
                _port = connection.Item2;
                _listenUrl = new Uri("http://localhost:" + _port);
                _userHandler = userHandler;
            }

            public void Dispose()
            {
                try { this._server.Stop(); }
                catch { }

                try { this._server.Dispose(); }
                catch { }
            }

            public bool WaitCallBack(TimeSpan maxWaitDelay) { return _eventRedirectComplete.WaitOne(maxWaitDelay); }

            public Uri ListenUri { get { return _listenUrl; } }

            private void RequestHandler(NHttp.HttpRequest request, NHttp.HttpResponse response, NHttp.HttpServerUtility utility)
            {
                if (_userHandler != null)
                    _userHandler(request, response, utility);

                _eventRedirectComplete.Set();
            }

            private static Tuple<NHttp.HttpServer, int> CreateAndStartHTTPServer(IEnumerable<int> tryPorts, Action<NHttp.HttpRequest, NHttp.HttpResponse, NHttp.HttpServerUtility> handler)
            {
                List<Exception> exs = new List<Exception>();
                foreach (int port in tryPorts)
                {
                    NHttp.HttpServer httpServer = null;
                    try
                    {
                        httpServer = new NHttp.HttpServer();
                        try
                        {
                            // Try port
                            httpServer.EndPoint = new IPEndPoint(IPAddress.Loopback, port);

                            // Request handling : 
                            httpServer.RequestReceived += (s, e) =>
                            {
                                handler(e.Request, e.Response, e.Server);
                            };

                            httpServer.Start();

                            return Tuple.Create(httpServer, port);
                        }
                        catch
                        {
                            if (httpServer != null)
                            {
                                try { httpServer.Stop(); }
                                catch { }
                            }

                            throw;
                        }
                    }
                    catch (Exception ex)
                    {
                        if (httpServer != null)
                            httpServer.Dispose();

                        exs.Add(ex);
                    }
                }

                throw new AggregateException("Cannot open any port", exs);
            }
        }

        private abstract class ApiInvoker : IDisposable
        {
            protected readonly string _apiMethod;
            protected readonly LinkedinApiClient _owner;
            private readonly Dictionary<string, string> _parameters = new Dictionary<string, string>();

            public ApiInvoker(LinkedinApiClient owner, string apiMethod)
            {
                if (owner == null)
                    throw new ArgumentNullException();

                if (string.IsNullOrWhiteSpace(apiMethod))
                    throw new ArgumentException();

                _owner = owner;
                _apiMethod = apiMethod;
            }

            public void Dispose()
            {
            }

            public string this[string param]
            {
                get
                {
                    if (string.IsNullOrWhiteSpace(param)) throw new ArgumentException();

                    if (_parameters.ContainsKey(param))
                    {
                        return _parameters[param];
                    }

                    return null;
                }
                set
                {
                    if (string.IsNullOrWhiteSpace(param)) throw new ArgumentException();

                    if (value != null)
                        _parameters[param] = value;
                    else
                    {
                        if (_parameters.ContainsKey(param))
                        {
                            _parameters.Remove(param);
                        }
                    }
                }
            }
        }

        private class ApiGetInvoker : ApiInvoker
        {
            public ApiGetInvoker(LinkedinApiClient owner, string apiMethod)
                : base(owner, apiMethod)
            {
            }

            public XDocument Invoke()
            {
                using (WebClient wc = new WebClient())
                {
                    wc.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + _owner.AccessToken);

                    string result = wc.DownloadString("https://api.linkedin.com" + _apiMethod);

                    return XDocument.Parse(result);
                }
            }
        }

        private class ApiPostInvoker : ApiInvoker
        {
            public ApiPostInvoker(LinkedinApiClient owner, string apiMethod)
                : base(owner, apiMethod)
            {
            }

            public XDocument Body { get; set; }

            public void Invoke(XDocument body)
            {
                using (WebClient wc = new WebClient())
                {
                    wc.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + _owner.AccessToken);
                    wc.Headers.Add(HttpRequestHeader.ContentType, "application/xml");

                    wc.UploadString("https://api.linkedin.com" + _apiMethod, body.ToString());
                }
            }
        }

        [System.Runtime.InteropServices.DllImport("user32.dll")]
        private static extern bool SetForegroundWindow(IntPtr hWnd);
    }
}
